Vite 分包策略 - 代码分割与按需加载
一、核心要点速览
💡 核心考点
- 分包目的: 减少初始加载体积,提升首屏速度
- 核心原理: Rollup 的代码分割机制 + 动态导入
- 常用策略: 路由分包、组件懒加载、第三方库拆分
- 配置关键:
manualChunks+dynamicImport
二、为什么需要分包
问题分析
单 Bundle 的问题:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 初始加载慢
所有代码打包成一个文件
用户需要下载全部代码才能使用
例如:app.js (2MB)
└─ 首页只需要 20% 的代码
└─ 其他 80% 被浪费
❌ 缓存效率低
任何代码修改都会改变整个 bundle 的 hash
用户需要重新下载所有代码
❌ 内存占用高
大量未使用的代码占用内存
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━分包的优势
分包后的效果:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ 减少初始加载
只加载当前页面需要的代码
首屏加载时间 ↓ 50-70%
✓ 按需加载
其他路由代码延迟加载
节省带宽和流量
✓ 缓存优化
每个 chunk 独立 hash
更新时只重新下载变化的部分
✓ 并行加载
多个小文件可以并行下载
利用浏览器并发能力
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━三、Vite 分包原理
Rollup 代码分割机制
┌──────────────────────────────────────────────────────────┐
│ Vite/Rollup 分包流程 │
└──────────────────────────────────────────────────────────┘
构建流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 解析入口文件
↓
2. 分析静态 import
├─ 基础依赖 → vendor chunk
├─ 页面组件 → pages chunk
└─ 共享代码 → common chunk
↓
3. 分析动态 import()
└─ 自动创建独立 chunk
↓
4. 应用 manualChunks 规则
└─ 按配置强制拆分
↓
5. 生成最终 chunks
├─ index.html
├─ assets/
│ ├── index.a1b2c3.js (入口)
│ ├── vendor.d4e5f6.js (第三方库)
│ ├── Home.g7h8i9.js (路由页面)
│ └── common.j0k1l2.js (共享代码)
└─ ...
关键点:
✓ 静态 import 自动分析
✓ 动态 import() 自动分割
✓ manualChunks 手动控制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Chunk 命名规则
javascript
// 默认命名格式
[文件名].[hash].[ext]
示例:
index.a1b2c3d4.js // 入口文件
vendor.e5f6g7h8.js // node_modules 依赖
Home.i9j0k1l2.js // 动态导入的页面
common.m3n4o5p6.js // 共享代码
带 hash 的好处:
✓ 长期缓存(hash 不变则缓存有效)
✓ 更新检测(内容变化 hash 变化)
✓ 避免缓存冲突四、分包策略详解
策略一:路由分包(最常用)
javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue') // 动态导入,自动分包
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue') // 独立 chunk
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue') // 独立 chunk
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// 构建结果:
// - dist/assets/Home.xxx.js
// - dist/assets/About.yyy.js
// - dist/assets/User.zzz.js效果对比:
| 方案 | 初始加载 | About 页面 | User 页面 |
|---|---|---|---|
| 不分包 | 2MB | 0ms | 0ms |
| 路由分包 | 500KB | 300KB | 400KB |
| 提升 | ↓ 75% | 按需加载 | 按需加载 |
策略二:组件懒加载
vue
<!-- 重组件按需加载 -->
<template>
<div>
<!-- 方式 1: 动态导入组件 -->
<component :is="HeavyComponent" />
<!-- 方式 2: 异步组件(推荐) -->
<AsyncChart />
<!-- 方式 3: 带 loading 状态的异步组件 -->
<AsyncEditor />
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
// 基础异步组件
HeavyComponent: () => import('@/components/HeavyComponent.vue'),
// 带配置的异步组件
AsyncChart: defineAsyncComponent({
loader: () => import('@/components/Chart.vue'),
loadingComponent: LoadingSpinner,
delay: 200, // 显示 loading 前的延迟
timeout: 3000 // 超时时间
}),
// 复杂场景
AsyncEditor: defineAsyncComponent(() => ({
component: import('@/components/Editor.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 5000
}))
}
}
</script>策略三:第三方库拆分
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
// 1. Vue 相关单独拆分
'vue-vendor': ['vue', 'vue-router', 'pinia'],
// 2. UI 库拆分
'element-plus': ['element-plus'],
'antd': ['ant-design-vue'],
// 3. 工具库拆分
'lodash': ['lodash-es'],
'dayjs': ['dayjs'],
// 4. 图表库(通常较大)
'echarts': ['echarts'],
// 5. 富文本编辑器
'editor': ['@wangeditor/editor', '@wangeditor/editor-for-vue']
}
}
}
}
})
// 构建结果:
// - dist/assets/vue-vendor.abc123.js
// - dist/assets/element-plus.def456.js
// - dist/assets/lodash.ghi789.js
// - dist/assets/echarts.jkl012.js进阶:函数形式更灵活
javascript
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 1. node_modules 中的依赖
if (id.includes('node_modules')) {
// Vue 生态
if (id.includes('vue') || id.includes('pinia')) {
return 'vue-vendor'
}
// UI 组件库
if (id.includes('element-plus')) {
return 'element-plus'
}
// 大型图表库
if (id.includes('echarts')) {
return 'charts'
}
// 其他统一放入 vendor
return 'vendor'
}
// 2. 项目中的共享组件
if (id.includes('src/components/common')) {
return 'common-components'
}
// 3. 工具函数
if (id.includes('src/utils')) {
return 'utils'
}
}
}
}
}
})策略四:大文件单独拆分
javascript
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id, { getModuleInfo }) {
// 获取模块大小
const moduleSize = getModuleInfo(id)?.size || 0
// 超过 50KB 的文件单独拆分
if (moduleSize > 50 * 1024) {
const fileName = id.split('/').pop()?.split('.')[0]
return `large-${fileName}`
}
// 特定页面单独拆分
if (id.includes('pages/heavy-page')) {
return 'heavy-page'
}
}
}
}
}
})五、完整配置示例
Vue 项目最佳实践
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
build: {
// 目标环境
target: 'es2015',
// 输出目录
outDir: 'dist',
// 代码分割配置
rollupOptions: {
output: {
// 自定义 chunk 命名
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
// 手动分包
manualChunks: {
// 1. 框架核心
'framework': [
'vue',
'vue-router',
'pinia'
],
// 2. UI 库
'ui': [
'element-plus',
'@element-plus/icons-vue'
],
// 3. 请求库
'request': [
'axios'
],
// 4. 工具库
'utils': [
'lodash-es',
'dayjs',
'crypto-js'
],
// 5. 可视化库
'charts': [
'echarts',
'zrender'
]
}
}
},
// 分包大小限制
chunkSizeWarningLimit: 500, // KB
// Gzip 压缩
gzipSize: true,
// 代码分割优化
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 生产环境移除 console
drop_debugger: true
}
}
}
})React 项目配置
javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
// React 核心
'react-core': ['react', 'react-dom', 'react-router-dom'],
// 状态管理
'state': ['redux', 'react-redux', '@reduxjs/toolkit'],
// UI 库
'antd': ['antd', '@ant-design/icons'],
// 工具
'utils': ['lodash-es', 'dayjs']
}
}
}
}
})六、分包效果分析
性能对比
┌──────────────────────────────────────────────────────────┐
│ 分包效果对比 │
└──────────────────────────────────────────────────────────┘
测试项目:中型电商网站(50+ 页面)
方案一:不分包
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始加载:████████████████████ 2.5MB
首屏时间:4.2 秒
LCP: 4.8 秒
方案二:路由分包
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始加载:████████ 800KB (-68%)
首屏时间:1.8 秒 (-57%)
LCP: 2.1 秒 (-56%)
方案三:精细分包
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始加载:████ 500KB (-80%)
首屏时间:1.2 秒 (-71%)
LCP: 1.5 秒 (-69%)
分包策略组合:
✓ 路由分包
✓ 第三方库拆分
✓ 组件懒加载
✓ 大文件单独处理
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Chunk 大小分布
优化后的 Chunk 分布:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
framework.js ████████ 150KB (Vue + Router + Pinia)
ui.js ████████████ 220KB (Element Plus)
utils.js ████ 80KB (Lodash + Dayjs)
charts.js ██████████ 180KB (ECharts)
index.js ██ 40KB (入口文件)
Home.js ████ 70KB (首页)
About.js ██ 30KB (关于页)
User.js ███ 50KB (用户页)
... 其他页面
总计:~820KB (gzip 后 ~280KB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━七、常见问题解决
问题排查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 分包太多 | manualChunks 过细 | 合并相似的 chunk,设置合理阈值 |
| 分包太少 | 未配置 dynamic import | 使用 () => import() 动态导入 |
| Chunk 重复 | 共享代码未提取 | 使用 experimentalOptimizeMinimize |
| Hash 频繁变化 | 共享代码包含业务逻辑 | 抽离纯工具代码到独立 chunk |
| 循环依赖 | 模块间相互引用 | 重构代码结构,打破循环 |
调试技巧
javascript
// 1. 查看分包结果
npm run build
// 2. 分析包体积
npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true
})
]
})
// 3. 查看网络请求
Chrome DevTools → Network
筛选 JS 文件,查看加载顺序和大小
// 4. 检查依赖关系
npx madge --circular src/八、面试标准回答
Vite 的分包主要依靠 Rollup 的代码分割机制,核心目的是减少初始加载体积,提升首屏速度。
分包的原理:Vite 在生产环境使用 Rollup 打包,Rollup 会自动分析代码中的静态 import 和动态 import()。对于动态导入的模块,Rollup 会自动创建独立的 chunk。我们也可以通过
manualChunks配置手动控制分包策略。常用的分包策略有四种:
- 路由分包:使用动态导入
() => import()让每个路由页面成为独立的 chunk- 组件懒加载:对大型组件使用
defineAsyncComponent异步加载- 第三方库拆分:通过
manualChunks将 node_modules 中的依赖按类型拆分- 大文件单独拆分:对超过阈值或特定的大文件单独打包
配置示例:
javascriptbuild: { rollupOptions: { output: { manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'element-plus': ['element-plus'], 'utils': ['lodash-es', 'dayjs'] } } } }实际项目中,我会:
- 首先使用路由分包,这是收益最大的
- 然后将大型第三方库(如 ECharts)单独拆分
- 对不常用的重型组件进行懒加载
- 最后用
rollup-plugin-visualizer分析包体积,进一步优化效果:通常能将初始加载体积减少 70-80%,首屏时间提升 50-70%。
九、记忆口诀
Vite 分包歌诀:
分包目的要记清,
减少加载是核心。
路由分包最常用,
动态导入自动分!
第三方库要拆分,
manualChunks 来帮忙。
Vue 库 UI 和工具,
各自独立缓存优!
组件太大懒加载,
defineAsyncComponent。
大文件也单独拆,
性能提升很明显!
配置完成看效果,
visualizer 来分析。
初始加载降下来,
首屏速度提上去!十、推荐资源
十一、总结一句话
Vite 分包: 动态导入 + manualChunks = 首屏加载快 70% ⚡